// Copyright (c) 2019 Presto Labs Pte. Ltd.
// Author: xguo, gekim

#pragma once

#include <iostream>
#include <memory>
#include <set>
#include <string>
#include <utility>

#include "coin2/base/log.h"
#include "coin2/base/time.h"
#include "coin2/exchange/base/symbology/symbol.h"
#include "coin2/exchange/base/symbology/symbology_spec.h"
#include "coin2/exchange/base/symbology/util.h"

namespace coin2::exchange::base::symbology {
const int64_t BCHA_BCHN_FORK_TIME =
    1605484800000000000L;  // TimestampFromDatetime(2020, 11, 16);

class ICurrency : public ISymbol {
 public:
  virtual ExchangeType exchange() const = 0;
  virtual const std::string& exchange_name() const = 0;

  virtual const std::string& feed_currency() const { return native_currency(); }
  virtual const std::string& order_currency() const {
    return native_currency();
  }

  virtual const std::string& currency() const = 0;
  virtual const std::string& native_currency() const = 0;
  virtual const std::string& native_currency(MarketType market_type) const = 0;
  virtual const std::string& rep_currency() const = 0;
  virtual ~ICurrency() = default;
};

class PseudoCurrency : public ICurrency {
 public:
  explicit PseudoCurrency(const std::string& symbol) : symbol_(symbol) {}

  const std::string& symbol() const override { return symbol_; }
  const std::string& native_symbol() const override { return symbol_; }

  ExchangeType exchange() const override { return {}; }
  const std::string& exchange_name() const override { return symbol_; }
  const std::string& currency() const override { return symbol_; }
  const std::string& native_currency() const override { return symbol_; }
  const std::string& native_currency(MarketType market_type) const override {
    return symbol_;
  }
  const std::string& rep_currency() const override { return symbol_; }

 private:
  std::string symbol_;
};

class Currency : public ICurrency {
 public:
  Currency(const std::string& currency, const std::string& native_currency,
           const std::string& rep_currency, const ExchangeType exchange)
      : currency_(currency),
        native_currency_(native_currency),
        rep_currency_(rep_currency),
        exchange_(exchange) {}

  Currency(const std::string& currency, const std::string& native_currency,
           const ExchangeType exchange)
      : Currency(currency, native_currency, GetRepCurrency(currency),
                 exchange) {}

 public:
  const std::string& symbol() const final { return currency_; }
  const std::string& native_symbol() const final { return native_currency_; }

  ExchangeType exchange() const final { return exchange_; }

  const std::string& exchange_name() const final {
    return market::ExchangeTypeToString(exchange_);
  }

  const std::string& currency() const final { return currency_; }

  const std::string& rep_currency() const final { return rep_currency_; }

  const std::string& native_currency() const final { return native_currency_; }

  // can be override
  const std::string& native_currency(
      MarketType /*market_type*/) const override {
    return native_currency_;
  }

  static const std::string& GetRepCurrency(const std::string& currency_str) {
    static std::set<std::string> usd_variants = {
        "USD",
        "USDT",  // USD Tether
        "GUSD",  // Gemini USD
        "HUSD",  // Huobi USD
        "PAX",   // Paxos Standard
        "TUSD",  // True USD
        "USDC",  // USD Coin
        "USDK",
    };
    static std::set<std::string> eur_variants = {
        "EUR",
        "EURT",  // EUR Tether
    };
    static std::string usd("USD");
    static std::string eur("EUR");

    if (usd_variants.find(currency_str) != usd_variants.end()) {
      return usd;
    } else if (eur_variants.find(currency_str) != eur_variants.end()) {
      return eur;
    } else {
      return currency_str;
    }
  }

 private:
  const std::string currency_;
  const std::string native_currency_;
  const std::string rep_currency_;
  const ExchangeType exchange_;
};

inline bool operator<(const ICurrency& lhs, const ICurrency& rhs) {
  const auto lhs_exchange = lhs.exchange();
  const auto rhs_exchange = rhs.exchange();
  if (lhs_exchange == rhs_exchange) {
    return lhs.currency() < rhs.currency();
  } else {
    return lhs_exchange < rhs_exchange;
  }
}

inline bool operator==(const ICurrency& lhs, const ICurrency& rhs) {
  const auto lhs_exchange = lhs.exchange();
  const auto rhs_exchange = rhs.exchange();
  if (lhs_exchange == rhs_exchange) {
    return lhs.currency() == rhs.currency();
  } else {
    return false;
  }
}

template <typename TCurrency>
class CurrencyFromStrMixin {
 public:
  // user implements
  // TCurrency::FromNormStrImpl
  // TCurrency::FromNativeStrImpl

  static std::unique_ptr<TCurrency> FromNormStrNoThrow(
      const std::string& currency_str, int64_t resolve_timestamp) {
    return TCurrency::FromNormStrImpl(currency_str, resolve_timestamp);
  }

  static std::unique_ptr<TCurrency> FromNativeStrNoThrow(
      const std::string& currency_str, int64_t resolve_timestamp) {
    return TCurrency::FromNativeStrImpl(currency_str, resolve_timestamp);
  }

  static TCurrency FromNormStr(const std::string& currency_str,
                               int64_t resolve_timestamp) {
    std::unique_ptr<TCurrency> currency =
        TCurrency::FromNormStrNoThrow(currency_str, resolve_timestamp);
    CHECK_THROW(currency) << "Unable to parse norm string " << currency_str;
    return std::move(*currency);
  }

  static TCurrency FromNativeStr(const std::string& currency_str,
                                 int64_t resolve_timestamp) {
    std::unique_ptr<TCurrency> currency =
        TCurrency::FromNativeStrNoThrow(currency_str, resolve_timestamp);
    CHECK_THROW(currency) << "Unable to parse native currency string: "
                          << currency_str;
    return std::move(*currency.get());
  }

  static std::unique_ptr<TCurrency> FromStrNoThrow(
      const std::string& currency_str, int64_t resolve_timestamp) {
    std::unique_ptr<TCurrency> currency;
    currency = TCurrency::FromNativeStrNoThrow(currency_str, resolve_timestamp);
    if (currency) return currency;
    currency = TCurrency::FromNormStrNoThrow(currency_str, resolve_timestamp);
    return currency;
  }

  static TCurrency FromStr(const std::string& currency_str,
                           int64_t resolve_timestamp) {
    std::unique_ptr<TCurrency> currency;
    currency = TCurrency::FromNativeStrNoThrow(currency_str, resolve_timestamp);
    if (currency) {
      return std::move(*currency);
    }
    currency = TCurrency::FromNormStrNoThrow(currency_str, resolve_timestamp);
    if (currency) {
      return std::move(*currency);
    }
    CHECK_THROW(currency) << "Unable to parse currency: " << currency_str
                          << " CurrencyType = " << typeid(TCurrency).name();
    return std::move(*currency);
  }
};

class CurrencyReplaceMeLater : public ICurrency {
 public:
  /* constructors
   * --------------------------------------------------------------------------
   */

  CurrencyReplaceMeLater(const std::string& currency,
                         const std::string& native_currency,
                         const std::string& rep_currency, ExchangeType exchange)
      : currency_(currency),
        native_currency_(native_currency),
        rep_currency_(rep_currency),
        exchange_(exchange) {}

  CurrencyReplaceMeLater(const std::string& currency, ExchangeType exchange,
                         const SymbologySpec& spec,
                         const ProductInfoConstant& pic)
      : CurrencyReplaceMeLater(currency, NormToNative(currency, spec, pic),
                               GetRepCurrency(currency), exchange) {}

  static CurrencyReplaceMeLater FromNative(
      const std::string& native_currency_str, int64_t resolve_timestamp,
      ExchangeType exchange, const SymbologySpec& spec,
      const ProductInfoConstant& pic) {
    auto currency =
        CurrencyReplaceMeLater::NativeToNorm(native_currency_str, spec, pic);
    return CurrencyReplaceMeLater(currency, exchange, spec, pic);
  }

  static CurrencyReplaceMeLater FromNorm(const std::string& norm_currency_str,
                                         int64_t resolve_timestamp,
                                         ExchangeType exchange,
                                         const SymbologySpec& spec,
                                         const ProductInfoConstant& pic) {
    auto currency =
        CurrencyReplaceMeLater::NormToNative(norm_currency_str, spec, pic);
    return CurrencyReplaceMeLater(StringToUpperCopy(norm_currency_str),
                                  exchange, spec, pic);
  }

  /* getters
   * --------------------------------------------------------------------------
   */

  const std::string& symbol() const final { return currency_; }
  const std::string& native_symbol() const final { return native_currency_; }
  ExchangeType exchange() const final { return exchange_; }
  const std::string& exchange_name() const final {
    return market::ExchangeTypeToString(exchange_);
  }
  const std::string& currency() const final { return currency_; }
  const std::string& rep_currency() const final { return rep_currency_; }
  const std::string& native_currency() const final { return native_currency_; }
  const std::string& native_currency(MarketType market_type) const final {
    throw std::runtime_error(
        "Currency::native_curency(market) not implemented");
  }

  static const std::string& GetRepCurrency(const std::string& currency_str) {
    static std::set<std::string> usd_variants = {
        "USD",
        "USDT",  // USD Tether
        "GUSD",  // Gemini USD
        "HUSD",  // Huobi USD
        "PAX",   // Paxos Standard
        "TUSD",  // True USD
        "USDC",  // USD Coin
        "USDK",
    };
    static std::set<std::string> eur_variants = {
        "EUR",
        "EURT",  // EUR Tether
    };
    static std::string usd("USD");
    static std::string eur("EUR");

    if (usd_variants.find(currency_str) != usd_variants.end()) {
      return usd;
    } else if (eur_variants.find(currency_str) != eur_variants.end()) {
      return eur;
    } else {
      return currency_str;
    }
  }

  /* conversions
   * --------------------------------------------------------------------------
   */

  static std::string NativeToNorm(const std::string& native_currency_str,
                                  const SymbologySpec& spec,
                                  const ProductInfoConstant& pic) {
    const auto native_currency_str_upper =
        StringToUpperCopy(native_currency_str);
    std::string norm_currency_str;
    auto it = spec.currencies.right.find(native_currency_str_upper);
    if (it != spec.currencies.right.end()) {
      // special currency name
      norm_currency_str = it->second;
    } else {
      // otherwise norm=native
      norm_currency_str = native_currency_str_upper;
    }

    if (pic.HasNormCurrency(norm_currency_str)) {
      return norm_currency_str;
    }
    throw std::runtime_error(
        fmt::format("fail to parse native symbol '{}' in ecxhange '{}'",
                    native_currency_str, "?"));
  }

  static std::string NormToNative(const std::string& norm_currency_str,
                                  const SymbologySpec& spec,
                                  const ProductInfoConstant& pic) {
    const auto norm_currency_str_upper = StringToUpperCopy(norm_currency_str);
    std::string native_currency_str;
    auto it = spec.currencies.left.find(norm_currency_str_upper);
    if (it != spec.currencies.left.end()) {
      // special currency name
      native_currency_str = it->second;
    } else {
      // otherwise native=norm
      native_currency_str = norm_currency_str_upper;
    }

    // Some market requires lower case symbol (e.g. Spot Huobi)
    if (spec.native_currency_lower_case) {
      StringToLowerInPlace(&native_currency_str);
    }

    if (pic.HasNativeCurrency(native_currency_str)) {
      return native_currency_str;
    }
    throw std::runtime_error(
        fmt::format("fail to parse native symbol '{}' in ecxhange '{}'",
                    norm_currency_str, "?"));
  }

 private:
  const std::string currency_;
  const std::string native_currency_;
  const std::string rep_currency_;
  const ExchangeType exchange_;
};

}  // namespace coin2::exchange::base::symbology

namespace std {

template <>
struct hash<coin2::exchange::base::symbology::ICurrency> {
  using ICurrency = coin2::exchange::base::symbology::ICurrency;
  std::size_t operator()(const ICurrency& currency) const {
    const auto h1 = static_cast<size_t>(currency.exchange());
    const auto h2 = std::hash<std::string>()(currency.symbol());
    return h1 ^ h2;
  }
};

/*
template<
  typename T,
  typename
std::enable_if<std::is_base_of<coin2::exchange::base::symbology::ICurrency,
T>::value, bool>::type = true > using hash =
hash<coin2::exchange::base::symbology::ICurrency>;
*/

}  // namespace std
